Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 84113c0990e0926cc5bc02e8361cab30b34f9b32


Parents : 0258901
Author : Mark Qvist <bc7291552be7a58f361522990465165c>
Signature : T66BB85Valid, signed by author
Date : 2026-06-09T20:39:29+02:00

Added AppImage build target

Changes

3 files changed, 732 insertions(+), 2 deletions(-)

M Makefile +30 -2

Diff

diff --git a/Makefile b/Makefile
index cec4f506..94f95e92 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,12 @@ clean:
cleanbuildozer:
make -C sbapp cleanall
-cleanall: clean cleanbuildozer
+cleanappimage:
+ @echo Cleaning AppImage build...
+ -rm -r ./build/appimage
+ -rm ./dist/Sideband-*.AppImage
+
+cleanall: clean cleanbuildozer cleanappimage
remove_symlinks:
@echo Removing symlinks for build...
@@ -88,12 +93,35 @@ build_winexe: prepare_win_pkg
release: build_wheel apk fetchapk
+prepare-appimage:
+ mkdir -p build/appimage_distwhls
+ LC_ALL=C $(MAKE) -C ../Reticulum clean debug
+ cp ../Reticulum/dist/rns-*-py3-none-any.whl build/appimage_distwhls/
+ LC_ALL=C $(MAKE) -C ../LXMF clean release
+ cp ../LXMF/dist/lxmf-*-py3-none-any.whl build/appimage_distwhls/
+ LC_ALL=C $(MAKE) -C ../LXST clean release
+ cp ../LXST/dist/lxst-*-py3-none-any.whl build/appimage_distwhls/
+
+appimage: appimage-x86_64
+
+appimage-x86_64: prepare-appimage
+ @echo Building Sideband AppImage for x86_64...
+ ./build_appimage.sh --arch x86_64
+
+appimage-aarch64: prepare-appimage
+ @echo Building Sideband AppImage for aarch64...
+ ./build_appimage.sh --arch aarch64
+
+appimage-clean:
+ @echo Cleaning AppImage build files...
+ -rm -rf build/appimage
+ -rm -f dist/Sideband-*.AppImage
+
upload:
@echo Ready to publish release over Reticulum
@read VOID
rngit release rns://7649a50d84610232d1416b41d2896aff/reticulum/sideband create $$(python setup.py --getversion):dist --name sideband
-
upload-pip:
@echo Ready to publish release, hit enter to continue
@read VOID

diff --git a/build_appimage.sh b/build_appimage.sh
new file mode 100755
index 00000000..bf5e1d86
--- /dev/null
+++ b/build_appimage.sh
@@ -0,0 +1,702 @@
+#!/bin/bash
+
+# Exit on error
+set -e
+
+# Configuration
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BUILD_DIR="${SCRIPT_DIR}/build/appimage"
+DIST_DIR="${SCRIPT_DIR}/dist"
+LXST_LIBS_DIR="${SCRIPT_DIR}/../LXST/lib/static"
+PYTHON_VERSION="3.12"
+PYTHON_TAG="cp312-cp312"
+ARCH="x86_64"
+LINUX_TAG="manylinux_2_28_x86_64"
+MANYLINUX_YEAR="2_28"
+CLEAN=0
+VERBOSE=0
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Logging functions
+log_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+log_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+log_warn() {
+ echo -e "${YELLOW}[WARN]${NC} $1"
+}
+
+log_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Help message
+show_help() {
+ cat << 'EOF'
+Sideband AppImage Build Script
+==============================
+This script builds a self-contained AppImage distribution of Sideband
+for Linux x86_64/aarch64 systems. It uses the python-appimage tool to
+create a relocatable Python environment and bundles all dependencies.
+
+Usage: ./build_appimage.sh [options]
+ -a, --arch ARCH Target architecture (x86_64 or aarch64), default: x86_64
+ -p, --python VERSION Python version to use, default: 3.12
+ -o, --output DIR Output directory for the AppImage, default: ./dist
+ -c, --clean Clean build directory before building
+ -v, --verbose Verbose output
+ -h, --help Show this help message
+
+Examples:
+ ./build_appimage.sh # Build for x86_64
+ ./build_appimage.sh --arch aarch64 # Build for ARM64
+ ./build_appimage.sh --clean # Clean and rebuild
+ make appimage # Build via Makefile
+EOF
+ exit 0
+}
+
+parse_args() {
+ while [[ $# -gt 0 ]]; do
+ case $1 in
+ -a|--arch)
+ ARCH="$2"
+ shift 2
+ ;;
+ -p|--python)
+ PYTHON_VERSION="$2"
+ PYTHON_TAG="cp${PYTHON_VERSION//./}-cp${PYTHON_VERSION//./}"
+ shift 2
+ ;;
+ -o|--output)
+ DIST_DIR="$2"
+ shift 2
+ ;;
+ -c|--clean)
+ CLEAN=1
+ shift
+ ;;
+ -v|--verbose)
+ VERBOSE=1
+ shift
+ ;;
+ -h|--help)
+ show_help
+ ;;
+ *)
+ log_error "Unknown option: $1"
+ show_help
+ exit 1
+ ;;
+ esac
+ done
+
+ # Set architecture-specific variables
+ if [[ "$ARCH" == "aarch64" ]]; then
+ LINUX_TAG="manylinux_2_28_aarch64"
+ MANYLINUX_YEAR="2_28"
+ elif [[ "$ARCH" != "x86_64" ]]; then
+ log_error "Unsupported architecture: $ARCH"
+ log_error "Supported architectures: x86_64, aarch64"
+ exit 1
+ fi
+}
+
+check_deps() {
+ log_info "Checking dependencies..."
+
+ if ! command -v python3 &> /dev/null; then
+ log_error "python3 is required but not installed"
+ exit 1
+ fi
+
+ if ! python3 -m python_appimage --help &> /dev/null; then
+ log_error "python-appimage package is required"
+ log_error "Install with: pip3 install python-appimage"
+ exit 1
+ fi
+
+ # Ensure appimagetool is available
+ if ! python3 -m python_appimage which appimagetool &> /dev/null; then
+ log_info "Installing appimagetool..."
+ python3 -m python_appimage install appimagetool
+ fi
+
+ log_success "Dependencies OK"
+}
+
+clean_build() {
+ if [[ $CLEAN -eq 1 ]] || [[ -d "$BUILD_DIR" ]]; then
+ log_info "Cleaning build directory..."
+ rm -rf "$BUILD_DIR"
+ log_success "Build directory cleaned"
+ fi
+}
+
+get_version() {
+ cd "$SCRIPT_DIR"
+ python3 setup.py --getversion 2>/dev/null || echo "unknown"
+}
+
+prepare_appdir() {
+ log_info "Preparing AppDir with Python $PYTHON_VERSION ($LINUX_TAG)..."
+
+ mkdir -p "$BUILD_DIR"
+ cd "$BUILD_DIR"
+
+ # Build the manylinux base if it doesn't exist
+ # The directory name includes the full Python version (e.g., python3.12.13-cp312-cp312-...)
+ local base_dir_pattern="python${PYTHON_VERSION}.*-${PYTHON_TAG}-${LINUX_TAG}"
+ local base_dir=$(find . -maxdepth 1 -type d -name "${base_dir_pattern}" | head -1 | sed 's|^\./||')
+
+ if [[ -z "$base_dir" ]] || [[ ! -d "$base_dir" ]]; then
+ log_info "Building Python base image (this may take a while)..."
+ python3 -m python_appimage build manylinux "${MANYLINUX_YEAR}_${ARCH}" "${PYTHON_TAG}" -n
+ # Find the newly created directory
+ base_dir=$(find . -maxdepth 1 -type d -name "${base_dir_pattern}" | head -1 | sed 's|^\./||')
+ fi
+
+ if [[ -z "$base_dir" ]] || [[ ! -d "$base_dir" ]]; then
+ log_error "Could not find or create Python base directory"
+ exit 1
+ fi
+
+ log_info "Using base directory: $base_dir"
+
+ # Copy to AppDir
+ if [[ -d "AppDir" ]]; then
+ rm -rf AppDir
+ fi
+ cp -r "$base_dir" AppDir
+
+ log_success "AppDir prepared"
+}
+
+install_pip_deps() {
+ log_info "Installing Python dependencies..."
+
+ local apprun="./AppDir/AppRun"
+ local site_packages="./AppDir/opt/python${PYTHON_VERSION}/lib/python${PYTHON_VERSION}/site-packages"
+
+ # Core dependencies
+ # pysdl2-dll provides manylinux2014-compatible SDL2 libraries
+ local edeps=(
+ "kivy>=2.3.0"
+ "numpy>=2.3.4"
+ "pillow>=10.2.0"
+ "mistune>=3.0.2"
+ "qrcode"
+ "materialyoucolor>=2.0.7"
+ "beautifulsoup4"
+ "pycodec2>=4.1.0"
+ "cffi>=2.0.0"
+ "cryptography>=3.4.7"
+ "pyserial>=3.5"
+ "pysdl2-dll>=2.32.0"
+ "prompt-toolkit"
+ )
+
+ local ldeps=(
+ "${BUILD_DIR}/../appimage_distwhls/rns-*-py3-none-any.whl"
+ "${BUILD_DIR}/../appimage_distwhls/lxmf-*-py3-none-any.whl"
+ "${BUILD_DIR}/../appimage_distwhls/lxst-*-py3-none-any.whl"
+ )
+
+ for dep in "${edeps[@]}"; do
+ log_info "Installing $dep..."
+ $apprun -m pip install --no-warn-script-location --quiet "$dep"
+ done
+
+ for dep in "${ldeps[@]}"; do
+ log_info "Installing $dep..."
+ $apprun -m pip install --no-warn-script-location --quiet $dep
+ done
+
+ log_success "Python dependencies installed"
+}
+
+copy_local_packages() {
+ log_info "Copying local packages..."
+
+ local site_packages="${BUILD_DIR}/AppDir/opt/python${PYTHON_VERSION}/lib/python${PYTHON_VERSION}/site-packages"
+
+ log_info "Copying sbapp..."
+ mkdir -p "$site_packages/sbapp"
+ tar -C "${SCRIPT_DIR}/sbapp" -cf - . 2>/dev/null | tar -C "$site_packages/sbapp" -xf -
+
+ for module in mapview kivymd md plyer pmqtt; do
+ if [[ -d "${SCRIPT_DIR}/sbapp/$module" ]]; then
+ log_info "Copying $module..."
+ rm -rf "$site_packages/$module"
+ cp -r "${SCRIPT_DIR}/sbapp/$module" "$site_packages/"
+ fi
+ done
+
+ cp "$LXST_LIBS_DIR/filterlib.cpython-311-aarch64-linux-gnu.so" "$site_packages/sbapp/LXST"
+ cp "$LXST_LIBS_DIR/filterlib.cpython-311-x86_64-linux-gnu.so" "$site_packages/sbapp/LXST"
+ cp "$LXST_LIBS_DIR/filterlib.cpython-312-aarch64-linux-gnu.so" "$site_packages/sbapp/LXST"
+ cp "$LXST_LIBS_DIR/filterlib.cpython-312-x86_64-linux-gnu.so" "$site_packages/LXST"
+ cp "$LXST_LIBS_DIR/filterlib.cpython-313-aarch64-linux-gnu.so" "$site_packages/sbapp/LXST"
+ cp "$LXST_LIBS_DIR/filterlib.cpython-313-x86_64-linux-gnu.so" "$site_packages/sbapp/LXST"
+ cp "$LXST_LIBS_DIR/filterlib.cpython-314-aarch64-linux-gnu.so" "$site_packages/sbapp/LXST"
+ cp "$LXST_LIBS_DIR/filterlib.cpython-314-x86_64-linux-gnu.so" "$site_packages/sbapp/LXST"
+
+ log_success "Local packages copied"
+}
+
+clean_sbapp() {
+ log_info "Cleaning sbapp package..."
+
+ local sbapp_dir="${BUILD_DIR}/AppDir/opt/python${PYTHON_VERSION}/lib/python${PYTHON_VERSION}/site-packages/sbapp"
+
+ rm -rf "$sbapp_dir/.buildozer"
+ rm -rf "$sbapp_dir/.gradle"
+ rm -rf "$sbapp_dir/bin"
+ rm -rf "$sbapp_dir/build"
+ rm -rf "$sbapp_dir/dist"
+ rm -rf "$sbapp_dir/patches"
+ rm -rf "$sbapp_dir/services"
+ rm -rf "$sbapp_dir/__pycache__"
+ rm -f "$sbapp_dir/buildozer.spec"
+ rm -f "$sbapp_dir/Makefile"
+
+ log_success "sbapp cleaned"
+}
+
+bundle_native_libs() {
+ log_info "Bundling native libraries..."
+
+ local lib_dir="${BUILD_DIR}/AppDir/usr/lib"
+ mkdir -p "$lib_dir"
+
+ # Extract pysdl2-dll libraries (manylinux2014 compatible = glibc 2.17)
+ local temp_dir="${BUILD_DIR}/temp_pysdl2"
+ mkdir -p "$temp_dir"
+
+ log_info "Downloading pysdl2-dll for compatible SDL2 libraries..."
+
+ # Download pysdl2-dll wheel (manylinux2014 compatible)
+ python3 -m pip download pysdl2-dll --platform manylinux2014_x86_64 --only-binary=:all: -d "$temp_dir" 2>/dev/null || \
+ python3 -m pip download pysdl2-dll -d "$temp_dir" 2>/dev/null || true
+
+ # Find the downloaded wheel
+ local wheel_file=$(find "$temp_dir" -name "pysdl2_dll*.whl" | head -1)
+ local sdl2_source=""
+
+ if [[ -n "$wheel_file" ]]; then
+ log_info "Extracting SDL2 libraries from pysdl2-dll..."
+ unzip -q "$wheel_file" -d "$temp_dir/extracted"
+ sdl2_source="$temp_dir/extracted/sdl2dll/dll"
+ fi
+
+ if [[ -d "$sdl2_source" ]]; then
+ log_info "Copying SDL2 libraries from pysdl2-dll..."
+
+ # Core SDL2 libraries
+ local sdl2_libs=(
+ "libSDL2-2.0.so"
+ "libSDL2-2.0.so.0"
+ "libSDL2_gfx-1.0.so"
+ "libSDL2_image-2.0.so"
+ "libSDL2_mixer-2.0.so"
+ "libSDL2_ttf-2.0.so"
+ )
+
+ # Audio codec libraries
+ local audio_libs=(
+ "libopus.so.0"
+ "libopusfile.so.0"
+ "libogg.so.0"
+ )
+
+ # Image format libraries
+ local image_libs=(
+ "libwebp.so.7"
+ "libtiff.so.5"
+ )
+
+ # Copy all available libraries
+ for lib in "${sdl2_libs[@]}" "${audio_libs[@]}" "${image_libs[@]}"; do
+ if [[ -f "$sdl2_source/$lib" ]]; then
+ cp -L "$sdl2_source/$lib" "$lib_dir/" 2>/dev/null
+ log_info " Copied $lib"
+ fi
+ done
+
+ # Copy any additional .so files that might be needed
+ for so_file in "$sdl2_source"/*.so*; do
+ if [[ -f "$so_file" ]]; then
+ local basename=$(basename "$so_file")
+ if [[ ! -f "$lib_dir/$basename" ]]; then
+ cp -L "$so_file" "$lib_dir/" 2>/dev/null || true
+ fi
+ fi
+ done
+
+ log_success "SDL2 libraries copied from pysdl2-dll"
+ else
+ log_warn "Could not find pysdl2-dll libraries, falling back to system libraries"
+
+ # Fallback: copy SDL2 from system
+ local sdl2_fallback_libs=(
+ "libSDL2-2.0.so.0"
+ "libSDL2_image-2.0.so.0"
+ "libSDL2_ttf-2.0.so.0"
+ "libSDL2_mixer-2.0.so.0"
+ )
+
+ for lib in "${sdl2_fallback_libs[@]}"; do
+ local lib_path=$(ldconfig -p | grep "$lib" | head -1 | awk '{print $NF}')
+ if [[ -n "$lib_path" ]] && [[ -f "$lib_path" ]]; then
+ log_info " Copying system $lib"
+ cp -L "$lib_path" "$lib_dir/" 2>/dev/null || true
+ fi
+ done
+ fi
+
+ # Clean up temp directory
+ rm -rf "$temp_dir"
+
+ # Handle codec2 separately - copy from system with compatibility warning
+ log_info "Checking for codec2 library..."
+ local codec2_path=$(ldconfig -p | grep "libcodec2.so.1" | head -1 | awk '{print $NF}')
+
+ if [[ -n "$codec2_path" ]] && [[ -f "$codec2_path" ]]; then
+ log_info "Copying codec2 from system"
+ cp -L "$codec2_path" "$lib_dir/" 2>/dev/null || true
+
+ # Also copy versioned files
+ for linked_lib in $(find $(dirname "$codec2_path") -name "libcodec2.so*" -type f -o -type l 2>/dev/null | head -3); do
+ cp -L "$linked_lib" "$lib_dir/" 2>/dev/null || true
+ done
+ else
+ log_warn "codec2 library not found - voice features will be unavailable"
+ fi
+
+ log_success "Native libraries bundled"
+}
+
+configure_apprun() {
+ log_info "Configuring AppRun..."
+
+ local apprun="${BUILD_DIR}/AppDir/AppRun"
+
+ cat > "$apprun" << 'EOF'
+#!/bin/bash
+#
+# Sideband AppImage Entry Point
+#
+
+# If running from an extracted image, set up environment
+if [ -z "${APPIMAGE}" ]; then
+ export ARGV0="$0"
+ self=$(readlink -f -- "$0")
+ here="${self%/*}"
+ tmp="${here%/*}"
+ export APPDIR="${tmp%/*}"
+fi
+
+# Resolve the calling command
+export APPIMAGE_COMMAND=$(command -v -- "$ARGV0")
+
+# Export Tcl/Tk paths
+export TCL_LIBRARY="${APPDIR}/usr/share/tcltk/tcl8.6"
+export TK_LIBRARY="${APPDIR}/usr/share/tcltk/tk8.6"
+export TKPATH="${TK_LIBRARY}"
+
+# Export SSL certificate
+export SSL_CERT_FILE="${APPDIR}/opt/_internal/certs.pem"
+
+# Set up Kivy/SDL2 environment
+export KIVY_SDL2_PATH="${APPDIR}/usr/lib"
+
+# Set library path for bundled libraries
+export LD_LIBRARY_PATH="${APPDIR}/usr/lib:${LD_LIBRARY_PATH}"
+
+# Set Python path
+export PYTHONPATH="${APPDIR}/opt/python3.12/lib/python3.12/site-packages:${PYTHONPATH}"
+
+# Launch Sideband
+"${APPDIR}/opt/python3.12/bin/python3.12" -m sbapp.main "$@"
+EOF
+
+ chmod +x "$apprun"
+
+ log_success "AppRun configured"
+}
+
+configure_desktop() {
+ log_info "Configuring desktop integration..."
+
+ local appdir="${BUILD_DIR}/AppDir"
+
+ # Remove old Python desktop files and AppStream metadata
+ rm -f "$appdir/python"*.desktop
+ rm -f "$appdir/usr/share/applications/python"*.desktop
+ rm -f "$appdir/usr/share/metainfo/python"*.appdata.xml
+
+ # Create desktop file
+ cat > "$appdir/sideband.desktop" << EOF
+[Desktop Entry]
+Name=Sideband
+Comment=Messaging, telemetry and remote control over LXMF
+Exec=AppRun
+Icon=sideband
+Categories=Utility;Network;
+Terminal=false
+Type=Application
+Version=1.0
+X-AppImage-Name=Sideband
+X-AppImage-Version=${VERSION}
+EOF
+
+ # Copy icon
+ cp "${SCRIPT_DIR}/sbapp/assets/icon.png" "$appdir/sideband.png"
+ cp "${SCRIPT_DIR}/sbapp/assets/icon_256.png" "$appdir/sideband_256.png"
+
+ # Set up symlinks for AppImage integration
+ rm -f "$appdir/.DirIcon"
+ ln -sf sideband.png "$appdir/.DirIcon"
+
+ # Install to usr/share directories
+ mkdir -p "$appdir/usr/share/applications"
+ mkdir -p "$appdir/usr/share/icons/hicolor/256x256/apps"
+ mkdir -p "$appdir/usr/share/icons/hicolor/512x512/apps"
+ cp "$appdir/sideband.desktop" "$appdir/usr/share/applications/"
+
+ find "$appdir/usr/share/icons" -name "python.png" -delete 2>/dev/null || true
+ rm -f "$appdir/python.png"
+ cp "$appdir/sideband_256.png" "$appdir/usr/share/icons/hicolor/256x256/apps/"
+ cp "$appdir/sideband.png" "$appdir/usr/share/icons/hicolor/512x512/apps/"
+
+ # Create minimal AppStream metadata to satisfy appimagetool
+ mkdir -p "$appdir/usr/share/metainfo"
+ cat > "$appdir/usr/share/metainfo/io.unsigned.sideband.metainfo.xml" << EOF
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop-application">
+ <id>io.unsigned.sideband</id>
+ <metadata_license>MIT</metadata_license>
+ <name>Sideband</name>
+ <summary>LXMF client for Reticulum networks</summary>
+ <description>
+ <p>Sideband is an LXMF client for Android, Linux and macOS allowing you to communicate with people or LXMF-compatible systems over Reticulum networks using LoRa, Packet Radio, WiFi, I2P, or anything else Reticulum supports.</p>
+ </description>
+ <url type="homepage">https://unsigned.io/sideband</url>
+ <launchable type="desktop-id">io.unsigned.sideband.desktop</launchable>
+ <provides>
+ <binary>sideband</binary>
+ </provides>
+</component>
+EOF
+
+ # Rename desktop file to match AppStream ID
+ mv "$appdir/usr/share/applications/sideband.desktop" "$appdir/usr/share/applications/io.unsigned.sideband.desktop"
+ cp "$appdir/usr/share/applications/io.unsigned.sideband.desktop" "$appdir/io.unsigned.sideband.desktop"
+ rm -f "$appdir/sideband.desktop"
+ ln -sf io.unsigned.sideband.desktop "$appdir/sideband.desktop" 2>/dev/null || true
+
+ log_success "Desktop integration configured"
+}
+
+patch_sitecustomize() {
+ log_info "Patching sitecustomize.py..."
+
+ local sitecustomize="${BUILD_DIR}/AppDir/opt/python${PYTHON_VERSION}/lib/python${PYTHON_VERSION}/site-packages/sitecustomize.py"
+
+ # Add our path hook at the beginning
+ cat > "$sitecustomize" << 'EOF'
+'''Python AppImage hooks for Sideband
+'''
+import atexit
+import os
+import sys
+
+# Ensure site-packages is in the path when running from AppImage
+_appdir = os.getenv('APPDIR')
+if _appdir:
+ _site_packages = os.path.join(_appdir, 'opt/python3.12/lib/python3.12/site-packages')
+ if _site_packages not in sys.path:
+ sys.path.insert(0, _site_packages)
+
+# Original python-appimage hooks
+_bin_at_start = os.listdir(sys.prefix + '/bin') if os.path.isdir(sys.prefix + '/bin') else []
+
+def patch_pip_install():
+ '''Change absolute shebangs to relative ones following a `pip` install
+ '''
+ if not 'pip' in sys.modules:
+ return
+
+ appdir = os.getenv('APPDIR')
+ if not appdir:
+ return
+
+ python_x_y = 'python{:}.{:}'.format(*sys.version_info[:2])
+ if sys.prefix != '{:}/opt/{:}'.format(appdir, python_x_y):
+ return
+
+ args = sys.argv[1:]
+ if 'install' in args:
+ for exe in os.listdir(sys.prefix + '/bin'):
+ path = os.path.join(sys.prefix, 'bin', exe)
+
+ if (not os.path.isfile(path)) or (not os.access(path, os.X_OK)) or \
+ exe.startswith('python') or os.path.islink(path) or \
+ exe.endswith('.pyc') or exe.endswith('.pyo'):
+ continue
+
+ try:
+ with open(path, 'r') as f:
+ header = f.read(2)
+ if header != '#!':
+ continue
+ content = f.read()
+ except:
+ continue
+
+ shebang, body = content.split(os.linesep, 1)
+ shebang = shebang.strip().split()
+ executable = shebang.pop(0)
+ if executable != sys.executable:
+ head, altbody = body.split(os.linesep, 1)
+ if head.startswith("'''exec' /"): # Patch for alt shebang
+ body = altbody.split(os.linesep, 1)[1]
+ executable = head.split()[1]
+ if executable != sys.executable:
+ continue
+ else:
+ continue
+
+ relpath = os.path.relpath(
+ sys.prefix + '/../../usr/bin/' + python_x_y,
+ sys.prefix + '/bin')
+ shebang.append('"$@"')
+ cmd = (
+ '"exec"',
+ '"$(dirname $(readlink -f ${0}))/' + relpath + '"',
+ '"$0"',
+ ' '.join(shebang)
+ )
+
+ try:
+ with open(path, 'w') as f:
+ f.write('#! /bin/sh\n')
+ f.write(' '.join(cmd) + '\n')
+ f.write(body)
+ except IOError:
+ continue
+
+ if exe in _bin_at_start:
+ continue
+
+ usr_dir = os.path.join(sys.prefix, '../../usr/bin')
+ usr_exe = os.path.join(usr_dir, exe)
+ if not os.path.exists(usr_exe):
+ relpath = os.path.relpath(path, usr_dir)
+ os.symlink(relpath, usr_exe)
+
+ elif 'uninstall' in args:
+ usr_dir = os.path.join(sys.prefix, '../../usr/bin')
+ if os.path.isdir(usr_dir):
+ for exe in os.listdir(usr_dir):
+ path = os.path.join(usr_dir, exe)
+ if (not os.path.islink(path)) or \
+ os.path.exists(os.path.realpath(path)):
+ continue
+ os.remove(path)
+
+
+if os.getenv('VIRTUAL_ENV') is None:
+ atexit.register(patch_pip_install)
+else:
+ del _bin_at_start
+ del patch_pip_install
+EOF
+
+ log_success "sitecustomize.py patched"
+}
+
+build_appimage() {
+ log_info "Building AppImage..."
+
+ mkdir -p "$DIST_DIR"
+
+ local version=$(get_version)
+ local output_name="Sideband-${version}-${ARCH}.AppImage"
+
+ cd "$BUILD_DIR"
+
+ # Get appimagetool path
+ local appimagetool
+ appimagetool=$(python3 -m python_appimage which appimagetool)
+ if [[ -z "$appimagetool" ]]; then
+ log_error "appimagetool not found"
+ exit 1
+ fi
+
+ # Build the AppImage
+ ARCH="$ARCH" "$appimagetool" AppDir "$output_name" 2>&1 | while read line; do
+ if [[ $VERBOSE -eq 1 ]]; then
+ echo "$line"
+ fi
+ done
+
+ # Move to dist directory
+ mv "$output_name" "$DIST_DIR/"
+
+ log_success "AppImage built: ${DIST_DIR}/${output_name}"
+
+ # Show file info
+ ls -lh "${DIST_DIR}/${output_name}"
+}
+
+# Main build process
+main() {
+ # Handle help before anything else
+ for arg in "$@"; do
+ if [[ "$arg" == "-h" ]] || [[ "$arg" == "--help" ]]; then
+ show_help
+ fi
+ done
+
+ log_info "Sideband AppImage Builder"
+ log_info "========================="
+
+ parse_args "$@"
+
+ log_info "Configuration:"
+ log_info " Architecture: $ARCH"
+ log_info " Python Version: $PYTHON_VERSION"
+ log_info " Build Directory: $BUILD_DIR"
+ log_info " Output Directory: $DIST_DIR"
+
+ VERSION=$(get_version)
+ log_info " Sideband Version: $VERSION"
+
+ check_deps
+ clean_build
+ prepare_appdir
+ install_pip_deps
+ copy_local_packages
+ clean_sbapp
+ bundle_native_libs
+ configure_apprun
+ configure_desktop
+ patch_sitecustomize
+ build_appimage
+
+ log_success "Build complete!"
+ log_info "Output: ${DIST_DIR}/Sideband-${VERSION}-${ARCH}.AppImage"
+}
+
+main "$@"

diff --git a/sbapp/assets/icon_256.png b/sbapp/assets/icon_256.png
new file mode 100644
index 00000000..06689b80
Binary files /dev/null and b/sbapp/assets/icon_256.png differ


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────